﻿//DerivativesHelper.cs
//Copyright (c) 2013 StockSharp LLC, all rights reserved.
//This code module is part of StockSharp library.
//This code is licensed under the GNU GENERAL PUBLIC LICENSE Version 3.
//See the file License.txt for the license details.
//More info on: http://stocksharp.com

namespace StockSharp.Algo.Derivatives
{
	using System;
	using System.Collections.Generic;
	using System.Globalization;
	using System.Linq;
	using System.Text.RegularExpressions;

	using Ecng.Collections;
	using Ecng.Common;

	using StockSharp.BusinessEntities;

	/// <summary>
	/// Вспомогательный класс для работы с производными финансовыми инструментами (деривативами).
	/// </summary>
	public static class DerivativesHelper
	{
		private static readonly Regex _futureNameRegex = new Regex(@"(?<code>[A-Z,a-z]+)-(?<expiryMonth>[0-9]{1,2})\.(?<expiryYear>[0-9]{1,2})", RegexOptions.Compiled);
		private static readonly Regex _optionNameRegex = new Regex(@"(?<code>\w+-[0-9]{1,2}\.[0-9]{1,2})(?<isMargin>[M_])(?<expiryDate>[0-9]{6,6})(?<optionType>[CP])(?<region>\w)\s(?<strike>\d*\.*\d*)", RegexOptions.Compiled);
		private static readonly Regex _optionCodeRegex = new Regex(@"(?<code>[A-Z,a-z]+)(?<strike>\d*\.*\d*)(?<optionType>[BA])(?<expiryMonth>[A-X]{1})(?<expiryYear>[0-9]{1})", RegexOptions.Compiled);

		private static readonly SynchronizedPairSet<int, char> _futureMonthCodes = new SynchronizedPairSet<int, char>();
		private static readonly SynchronizedPairSet<int, char> _optionCallMonthCodes = new SynchronizedPairSet<int, char>();
		private static readonly SynchronizedPairSet<int, char> _optionPutMonthCodes = new SynchronizedPairSet<int, char>();

		static DerivativesHelper()
		{
			// http://www.rts.ru/s193
			_futureMonthCodes.Add(1, 'F');
			_futureMonthCodes.Add(2, 'G');
			_futureMonthCodes.Add(3, 'H');
			_futureMonthCodes.Add(4, 'J');
			_futureMonthCodes.Add(5, 'K');
			_futureMonthCodes.Add(6, 'M');
			_futureMonthCodes.Add(7, 'N');
			_futureMonthCodes.Add(8, 'Q');
			_futureMonthCodes.Add(9, 'U');
			_futureMonthCodes.Add(10, 'V');
			_futureMonthCodes.Add(11, 'X');
			_futureMonthCodes.Add(12, 'Z');

			_optionCallMonthCodes.Add(1, 'A');
			_optionCallMonthCodes.Add(2, 'B');
			_optionCallMonthCodes.Add(3, 'C');
			_optionCallMonthCodes.Add(4, 'D');
			_optionCallMonthCodes.Add(5, 'E');
			_optionCallMonthCodes.Add(6, 'F');
			_optionCallMonthCodes.Add(7, 'G');
			_optionCallMonthCodes.Add(8, 'H');
			_optionCallMonthCodes.Add(9, 'I');
			_optionCallMonthCodes.Add(10, 'J');
			_optionCallMonthCodes.Add(11, 'K');
			_optionCallMonthCodes.Add(12, 'L');

			_optionPutMonthCodes.Add(1, 'M');
			_optionPutMonthCodes.Add(2, 'N');
			_optionPutMonthCodes.Add(3, 'O');
			_optionPutMonthCodes.Add(4, 'P');
			_optionPutMonthCodes.Add(5, 'Q');
			_optionPutMonthCodes.Add(6, 'R');
			_optionPutMonthCodes.Add(7, 'S');
			_optionPutMonthCodes.Add(8, 'T');
			_optionPutMonthCodes.Add(9, 'U');
			_optionPutMonthCodes.Add(10, 'V');
			_optionPutMonthCodes.Add(11, 'W');
			_optionPutMonthCodes.Add(12, 'X');
		}

		private static readonly SynchronizedDictionary<Security, Security> _underlyingSecurities = new SynchronizedDictionary<Security, Security>();

		/// <summary>
		/// Получить базовый актив по деривативу.
		/// </summary>
		/// <param name="derivative">Дериватив.</param>
		/// <returns>Базовый актив.</returns>
		public static Security GetUnderlyingAsset(this Security derivative)
		{
			var trader = derivative.CheckTrader();

			if (derivative.Type == SecurityTypes.Option)
			{
				derivative.CheckOption();

				return _underlyingSecurities.SafeAdd(derivative, key =>
				{
					var underlyingSecurity = trader.Securities.FirstOrDefault(s => s.Id == key.UnderlyingSecurityId);

					if (underlyingSecurity == null)
						throw new InvalidOperationException("Инструмент {0} не найден.".Put(key.UnderlyingSecurityId));

					return underlyingSecurity;
				});
			}
			else
			{
				return trader.Securities.FirstOrDefault(s => s.Id == derivative.UnderlyingSecurityId);
			}
		}

		/// <summary>
		/// Отфильтровать опционы по страйку <see cref="Security.Strike"/>.
		/// </summary>
		/// <param name="options">Опционы, которые необходимо отфильтровать.</param>
		/// <param name="strike">Цена страйка.</param>
		/// <returns>Отфильтрованные опционы.</returns>
		public static IEnumerable<Security> Filter(this IEnumerable<Security> options, decimal strike)
		{
			return options.Where(o => o.Strike == strike);
		}

		/// <summary>
		/// Отфильтровать опционы по типу <see cref="Security.OptionType"/>.
		/// </summary>
		/// <param name="options">Опционы, которые необходимо отфильтровать.</param>
		/// <param name="type">Тип опциона.</param>
		/// <returns>Отфильтрованные опционы.</returns>
		public static IEnumerable<Security> Filter(this IEnumerable<Security> options, OptionTypes type)
		{
			return options.Where(o => o.OptionType == type);
		}

		/// <summary>
		/// Отфильтровать инструменты по базовому активу.
		/// </summary>
		/// <param name="securities">Инструменты, которые необходимо отфильтровать.</param>
		/// <param name="asset">Базовый актив.</param>
		/// <returns>Отфильтрованные инструменты.</returns>
		public static IEnumerable<Security> Filter(this IEnumerable<Security> securities, Security asset)
		{
			if (asset == null)
				throw new ArgumentNullException("asset");

			return securities.Where(s => s.UnderlyingSecurityId == asset.Id);
		}

		/// <summary>
		/// Отфильтровать инструменты по дате экспирации <see cref="Security.ExpiryDate"/>.
		/// </summary>
		/// <param name="securities">Инструменты, которые необходимо отфильтровать.</param>
		/// <param name="expidationDate">Дата экспирации.</param>
		/// <returns>Отфильтрованные инструменты.</returns>
		public static IEnumerable<Security> Filter(this IEnumerable<Security> securities, DateTime? expidationDate)
		{
			if (expidationDate == null)
				return securities;

			return securities.Where(s => s.ExpiryDate == expidationDate);
		}

		/// <summary>
		/// Получить деривативы по базовому активу.
		/// </summary>
		/// <param name="asset">Базовый актив.</param>
		/// <param name="expidationDate">Дата экспирации.</param>
		/// <returns>Список из деривативов.</returns>
		/// <remarks>Возвращает пустой список, если деривативов не найдено.</remarks>
		public static IEnumerable<Security> GetDerivatives(this Security asset, DateTime? expidationDate = null)
		{
			return asset.CheckTrader().Securities.Filter(asset).Filter(expidationDate);
		}

		/// <summary>
		/// Получить базовый актив.
		/// </summary>
		/// <param name="derivative">Дериватив.</param>
		/// <returns>Базовый актив.</returns>
		public static Security GetAsset(this Security derivative)
		{
			var asset = derivative.CheckTrader().Securities.FirstOrDefault(s => s.Id == derivative.UnderlyingSecurityId);

			if (asset == null)
				throw new ArgumentException("Для дериватива {0} не было найдено базового актива.".Put(derivative));

			return asset;
		}

		/// <summary>
		/// Поменять тип опциона на противоположное.
		/// </summary>
		/// <param name="type">Первоначальное значение.</param>
		/// <returns>Противоположное значение.</returns>
		public static OptionTypes Invert(this OptionTypes type)
		{
			return type == OptionTypes.Call ? OptionTypes.Put : OptionTypes.Call;
		}

		/// <summary>
		/// Получить противоположный опцион (для Call получить Put, для Put получить Call).
		/// </summary>
		/// <param name="option">Опцион.</param>
		/// <returns>Противоположный опцион.</returns>
		public static Security GetOppositeOption(this Security option)
		{
			option.CheckOption();

			var optionType = option.OptionType == OptionTypes.Call ? OptionTypes.Put : OptionTypes.Call;

			var oppositeOption = option.Trader.Securities
				.Filter(option.Strike)
				.Filter(optionType)
				.Filter(option.ExpiryDate)
				.Filter(GetUnderlyingAsset(option))
				.FirstOrDefault();

			if (oppositeOption == null)
				throw new ArgumentException("Противоположный опцион для {0} не найден.".Put(option.Id), "option");

			return oppositeOption;
		}

		/// <summary>
		/// Получить Call для базового фьючерса.
		/// </summary>
		/// <param name="future">Базовый фьючерс.</param>
		/// <param name="strike">Страйк.</param>
		/// <param name="expidationDate">Дата экспирации опциона.</param>
		/// <returns>Опцион Call.</returns>
		public static Security GetCall(this Security future, decimal strike, DateTime expidationDate)
		{
			return future.GetOption(strike, expidationDate, OptionTypes.Call);
		}

		/// <summary>
		/// Получить Put для базового фьючерса.
		/// </summary>
		/// <param name="future">Базовый фьючерс.</param>
		/// <param name="strike">Страйк.</param>
		/// <param name="expirationDate">Дата экспирации опциона.</param>
		/// <returns>Опцион Put.</returns>
		public static Security GetPut(this Security future, decimal strike, DateTime expirationDate)
		{
			return future.GetOption(strike, expirationDate, OptionTypes.Put);
		}

		/// <summary>
		/// Получить опцион для базового фьючерса.
		/// </summary>
		/// <param name="future">Базовый фьючерс.</param>
		/// <param name="strike">Страйк.</param>
		/// <param name="expidationDate">Дата экспирации опционов.</param>
		/// <param name="optionType">Тип опциона.</param>
		/// <returns>Опцион.</returns>
		public static Security GetOption(this Security future, decimal strike, DateTime expidationDate, OptionTypes optionType)
		{
			var option = future.CheckTrader().Securities
				.Filter(strike)
				.Filter(optionType)
				.Filter(expidationDate)
				.Filter(future)
				.FirstOrDefault();

			if (option == null)
				throw new ArgumentException("Опцион для {0} не найден.".Put(future.Id), "future");

			return option;
		}

		/// <summary>
		/// Получить центральный страйк.
		/// </summary>
		/// <param name="underlyingAsset">Базовый актив.</param>
		/// <param name="expidationDate">Дата экспирации опционов.</param>
		/// <param name="optionType">Тип опциона.</param>
		/// <returns>Центральный страйк.</returns>
		public static Security GetCentralStrike(this Security underlyingAsset, DateTime expidationDate, OptionTypes optionType)
		{
			return underlyingAsset.GetCentralStrike(underlyingAsset.GetDerivatives(expidationDate).Filter(optionType));
		}

		/// <summary>
		/// Получить центральный страйк.
		/// </summary>
		/// <param name="underlyingAsset">Базовый актив.</param>
		/// <param name="allStrikes">Все страйки.</param>
		/// <returns>Центральный страйк.</returns>
		public static Security GetCentralStrike(this Security underlyingAsset, IEnumerable<Security> allStrikes)
		{
			var assetPrice = underlyingAsset.GetCurrentPrice();
			return allStrikes.OrderBy(s => Math.Abs((decimal)(s.Strike - assetPrice))).FirstOrDefault();
		}

		/// <summary>
		/// Получить размер шага страйка.
		/// </summary>
		/// <param name="underlyingAsset">Базовый актив.</param>
		/// <param name="expidationDate">Дата экспирации опционов (для указания конкретной серии).</param>
		/// <returns>Размер шага страйка.</returns>
		public static decimal GetStrikeStep(this Security underlyingAsset, DateTime? expidationDate = null)
		{
			var group = underlyingAsset
				.GetDerivatives(expidationDate)
				.Filter(OptionTypes.Call)
				.GroupBy(s => s.ExpiryDate)
				.FirstOrDefault();

			if (group == null)
				throw new InvalidOperationException("Недостаточно информации для определения шага страйка.");

			var orderedStrikes = group.OrderBy(s => s.Strike).Take(2).ToArray();
			return orderedStrikes[1].Strike - orderedStrikes[0].Strike;
		}

		/// <summary>
		/// Получить опционы вне денег (OTM).
		/// </summary>
		/// <param name="underlyingAsset">Базовый актив.</param>
		/// <returns>Опционы вне денег.</returns>
		public static IEnumerable<Security> GetOutOfTheMoney(this Security underlyingAsset)
		{
			return underlyingAsset.GetOutOfTheMoney(underlyingAsset.GetDerivatives());
		}

		/// <summary>
		/// Получить опционы вне денег (OTM).
		/// </summary>
		/// <param name="underlyingAsset">Базовый актив.</param>
		/// <param name="allStrikes">Все страйки.</param>
		/// <returns>Опционы вне денег.</returns>
		public static IEnumerable<Security> GetOutOfTheMoney(this Security underlyingAsset, IEnumerable<Security> allStrikes)
		{
			if (underlyingAsset == null)
				throw new ArgumentNullException("underlyingAsset");

			var cs = underlyingAsset.GetCentralStrike(allStrikes);

			return allStrikes.Where(s => s.OptionType == OptionTypes.Call ? s.Strike < cs.Strike : s.Strike > cs.Strike);
		}

		/// <summary>
		/// Получить опционы в деньгах (ITM).
		/// </summary>
		/// <param name="underlyingAsset">Базовый актив.</param>
		/// <returns>Опционы в деньгах.</returns>
		public static IEnumerable<Security> GetInTheMoney(this Security underlyingAsset)
		{
			return underlyingAsset.GetInTheMoney(underlyingAsset.GetDerivatives());
		}

		/// <summary>
		/// Получить опционы в деньгах (ITM).
		/// </summary>
		/// <param name="underlyingAsset">Базовый актив.</param>
		/// <param name="allStrikes">Все страйки.</param>
		/// <returns>Опционы в деньгах.</returns>
		public static IEnumerable<Security> GetInTheMoney(this Security underlyingAsset, IEnumerable<Security> allStrikes)
		{
			if (underlyingAsset == null)
				throw new ArgumentNullException("underlyingAsset");

			var cs = underlyingAsset.GetCentralStrike(allStrikes);

			return allStrikes.Where(s => s.OptionType == OptionTypes.Call ? s.Strike > cs.Strike : s.Strike < cs.Strike);
		}

		/// <summary>
		/// Получить опционы на деньгах (ATM).
		/// </summary>
		/// <param name="underlyingAsset">Базовый актив.</param>
		/// <returns>Опционы на деньгах.</returns>
		public static IEnumerable<Security> GetAtTheMoney(this Security underlyingAsset)
		{
			return underlyingAsset.GetAtTheMoney(underlyingAsset.GetDerivatives());
		}

		/// <summary>
		/// Получить опционы на деньгах (ATM).
		/// </summary>
		/// <param name="underlyingAsset">Базовый актив.</param>
		/// <param name="allStrikes">Все страйки.</param>
		/// <returns>Опционы на деньгах.</returns>
		public static IEnumerable<Security> GetAtTheMoney(this Security underlyingAsset, IEnumerable<Security> allStrikes)
		{
			if (underlyingAsset == null)
				throw new ArgumentNullException("underlyingAsset");

			var centralStrikes = new List<Security>();

			var cs = underlyingAsset.GetCentralStrike(allStrikes.Filter(OptionTypes.Call));

			if (cs != null)
				centralStrikes.Add(cs);

			cs = underlyingAsset.GetCentralStrike(allStrikes.Filter(OptionTypes.Put));

			if (cs != null)
				centralStrikes.Add(cs);

			return centralStrikes;
		}

		/// <summary>
		/// Получить внутреннюю стоимость опциона.
		/// </summary>
		/// <param name="option">Опцион.</param>
		/// <returns>Внутренняя стоимость.</returns>
		public static decimal GetIntrinsicValue(this Security option)
		{
			option.CheckOption();

			var assetPrice = option.GetUnderlyingAsset().GetCurrentPrice();

			return ((decimal)((option.OptionType == OptionTypes.Call) ? assetPrice - option.Strike : option.Strike - assetPrice)).Max(0);
		}

		/// <summary>
		/// Получить временную стоимость опциона.
		/// </summary>
		/// <param name="option">Опцион.</param>
		/// <returns>Временная стоимость.</returns>
		public static decimal GetTimeValue(this Security option)
		{
			option.CheckOption();

			return (decimal)(option.GetCurrentPrice() - option.GetUnderlyingAsset().GetCurrentPrice());
		}

		internal static DateTime GetExpirationTime(this Security security)
		{
			security.CheckTrader();

			if (security.ExpiryDate == null)
				throw new ArgumentException("Инструмент {0} не имеет информацию о дате экспирации.".Put(security.Id), "security");

			var expDate = (DateTime)security.ExpiryDate;

			if (expDate.TimeOfDay == TimeSpan.Zero)
				expDate += security.CheckExchangeBoard().ExpiryTime;

			return expDate;
		}

		/// <summary>
		/// Проверить, закончил ли действие инструмент.
		/// </summary>
		/// <param name="security">Инструмент.</param>
		/// <returns>True, если инструмент закончил свое действие.</returns>
		public static bool IsExpired(this Security security)
		{
			return security.GetExpirationTime() >= security.GetMarketTime();
		}

		/// <summary>
		/// Получить из названия опциона его информацию (базовый актив, страйк, дата экспирации и т.д.).
		/// </summary>
		/// <param name="optionName">Название опциона.</param>
		/// <returns>Информация об опционе.</returns>
		public static Security GetOptionInfo(this string optionName)
		{
			if (optionName.IsEmpty())
				throw new ArgumentNullException("optionName");

			var matches = _optionNameRegex.Matches(optionName);

			if (matches.Count == 1)
			{
				var groups = matches[0].Groups;

				if (groups.Count == 7)
				{
					return new Security
					{
						UnderlyingSecurityId = groups["code"].Value,
						ExpiryDate = groups["expiryDate"].Value.ToDateTime("ddMMyy"),
						OptionType = groups["optionType"].Value == "C" ? OptionTypes.Call : OptionTypes.Put,
						Strike = decimal.Parse(groups["strike"].Value, CultureInfo.InvariantCulture),
					};
				}
			}

			return null;
		}

		/// <summary>
		/// Получить из названия фьючерса его информацию (базовый актив, дата экспирации и т.д.).
		/// </summary>
		/// <param name="futureName">Название фьючерса.</param>
		/// <param name="optionCode">Код опциона.</param>
		/// <returns>Информация о фьючерсе.</returns>
		public static Security GetFutureInfo(this string futureName, string optionCode)
		{
			if (futureName.IsEmpty())
				throw new ArgumentNullException("futureName");

			if (optionCode.IsEmpty())
				throw new ArgumentNullException("optionCode");

			var matches = _futureNameRegex.Matches(futureName);

			if (matches.Count == 1)
			{
				var groups = matches[0].Groups;

				if (groups.Count == 4)
				{
					var yearStr = groups["expiryYear"].Value;
					var month = groups["expiryMonth"].Value.To<int>();

					var optionMatch = _optionCodeRegex.Match(optionCode);

					return new Security
					{
						//Name = groups["code"].Value,
						Code = optionMatch.Groups["code"].Value + _futureMonthCodes[month] + yearStr.Last(),
						ExpiryDate = new DateTime(2000 + yearStr.To<int>(), month, 1),
						Name = futureName,
					};
				}
			}

			return null;
		}

		/// <summary>
		/// Создать стакан волатильности из обычного стакана.
		/// </summary>
		/// <param name="depth">Стакан, котировки которого будут переведены в котировки с волатильностью.</param>
		/// <param name="riskFree">Безрисковая процентная ставка.</param>
		/// <param name="dividend">Размер дивиденда по акциям.</param>
		/// <returns>Стакан волатильности.</returns>
		public static MarketDepth IV(this MarketDepth depth, decimal riskFree = 0, decimal dividend = 0)
		{
			if (depth == null)
				throw new ArgumentNullException("depth");

			return depth.IV(new BlackScholes(depth.Security) { RiskFree = riskFree, Dividend = dividend });
		}

		/// <summary>
		/// Создать стакан волатильности из обычного стакана.
		/// </summary>
		/// <param name="depth">Стакан, котировки которого будут переведены в котировки с волатильностью.</param>
		/// <param name="model">Модель расчета значений "греков" по формуле Блэка-Шоулза.</param>
		/// <returns>Стакан волатильности.</returns>
		public static MarketDepth IV(this MarketDepth depth, BlackScholes model)
		{
			if (depth == null)
				throw new ArgumentNullException("depth");

			if (model == null)
				throw new ArgumentNullException("model");

			Func<Quote, Quote> convert = quote =>
			{
				quote = quote.Clone();
				quote.Price = model.IV(quote.Price);
				return quote;
			};

			return new MarketDepth(depth.Security).Update(depth.Bids.Select(convert), depth.Asks.Select(convert), true, depth.LastChangeTime);
		}
	}
}